#include <mmd/mmd.h>

GLuint CreateShader(GLenum shaderType, const string code)
{
	GLuint shader = glCreateShader(shaderType);
	if (shader == 0)
	{
		cout << "Failed to create shader.\n";
		return 0;
	}
	const char* codes[] = { code.c_str() };
	GLint codesLen[] = { (GLint)code.size() };
	glShaderSource(shader, 1, codes, codesLen);
	glCompileShader(shader);

	GLint infoLength;
	glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLength);
	if (infoLength != 0)
	{
		vector<char> info;
		info.reserve(infoLength + 1);
		info.resize(infoLength);

		GLsizei len;
		glGetShaderInfoLog(shader, infoLength, &len, &info[0]);
		if (info[infoLength - 1] != '\0')
		{
			info.push_back('\0');
		}

		cout << &info[0] << "\n";
	}

	GLint compileStatus;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
	if (compileStatus != GL_TRUE)
	{
		glDeleteShader(shader);
		cout << "Failed to compile shader.\n";
		return 0;
	}

	return shader;
}

GLuint CreateShaderProgram(const string vsFile, const string fsFile)
{
	saba::TextFileReader vsFileText;
	if (!vsFileText.Open(vsFile))
	{
		cout << "Failed to open shader file. [" << vsFile << "].\n";
		return 0;
	}
	string vsCode = vsFileText.ReadAll();
	vsFileText.Close();

	saba::TextFileReader fsFileText;
	if (!fsFileText.Open(fsFile))
	{
		cout << "Failed to open shader file. [" << fsFile << "].\n";
		return 0;
	}
	string fsCode = fsFileText.ReadAll();
	fsFileText.Close();

	GLuint vs = CreateShader(GL_VERTEX_SHADER, vsCode);
	GLuint fs = CreateShader(GL_FRAGMENT_SHADER, fsCode);
	if (vs == 0 || fs == 0)
	{
		if (vs != 0) { glDeleteShader(vs); }
		if (fs != 0) { glDeleteShader(fs); }
		return 0;
	}

	GLuint prog = glCreateProgram();
	if (prog == 0)
	{
		glDeleteShader(vs);
		glDeleteShader(fs);
		cout << "Failed to create program.\n";
		return 0;
	}
	glAttachShader(prog, vs);
	glAttachShader(prog, fs);
	glLinkProgram(prog);

	GLint infoLength;
	glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &infoLength);
	if (infoLength != 0)
	{
		vector<char> info;
		info.reserve(infoLength + 1);
		info.resize(infoLength);

		GLsizei len;
		glGetProgramInfoLog(prog, infoLength, &len, &info[0]);
		if (info[infoLength - 1] != '\0')
		{
			info.push_back('\0');
		}

		cout << &info[0] << "\n";
	}

	GLint linkStatus;
	glGetProgramiv(prog, GL_LINK_STATUS, &linkStatus);
	if (linkStatus != GL_TRUE)
	{
		glDeleteShader(vs);
		glDeleteShader(fs);
		cout << "Failed to link shader.\n";
		return 0;
	}

	glDeleteShader(vs);
	glDeleteShader(fs);
	return prog;
}

AppContext * AppContext::m_instance = nullptr;

AppContext * AppContext::getInstance(){
	if(m_instance == nullptr)m_instance = new AppContext();
	return m_instance;
}

AppContext::AppContext():
	Config("resource/config/mmd.json"),
	IAgentComponent(AgentComponentType::INTERFACE,true){
	m_instance = this;
    Json::Value light = getValue("lightColor");
    m_lightColor = glm::vec3(light[0].asFloat(),light[1].asFloat(),light[2].asFloat());
    show_shadow = getValue("shadow").asBool();
    show_edge = getValue("edge").asBool();
	month_speed = getValue("month_speed").asFloat();
	Blink_interval = getValue("Blink_interval").asFloat();
	Blink_speed = getValue("Blink_speed").asFloat();
	Blink_last = getValue("Blink_last").asFloat();
	showPanel = getValue("showPanel").asBool();
}

void AppContext::Init(){
	window = agent->GetWindow();
	glfwMakeContextCurrent(window);

	// Setup resource directory.
	m_resourceDir = "./resource";
	m_shaderDir = "./resource/shader";
	m_mmdDir = "./resource/mmd";

	m_mmdShader = make_unique<MMDShader>();
	if (!m_mmdShader->Setup(*this))return;

	m_mmdEdgeShader = make_unique<MMDEdgeShader>();
	if (!m_mmdEdgeShader->Setup(*this))return;

	m_mmdGroundShadowShader = make_unique<MMDGroundShadowShader>();
	if (!m_mmdGroundShadowShader->Setup(*this))return;

	glGenTextures(1, &m_dummyColorTex);
	glBindTexture(GL_TEXTURE_2D, m_dummyColorTex);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
	glBindTexture(GL_TEXTURE_2D, 0);

	glGenTextures(1, &m_dummyShadowDepthTex);
	glBindTexture(GL_TEXTURE_2D, m_dummyShadowDepthTex);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 1, 1, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
	glBindTexture(GL_TEXTURE_2D, 0);

	AddModel("Agent","resource/model/rem/rem.pmx",glm::vec3(0,0,0));
	InitMorph();
	//testAnim =  GetAnimation("Agent","resource/motion/mei_idle/mei_idle_boredom.vmd");
	//SetAnimation("Agent",testAnim,true);
    vmds["talk0"] = GetAnimation("resource/motion/mei_guide_happy.vmd");
    vmds["talk1"] = GetAnimation("resource/motion/mei_self_introduction.vmd");
    vmds["idle0"] = GetAnimation("resource/motion/mei_idle_boredom.vmd");
    vmds["idle1"] = GetAnimation("resource/motion/mei_idle_sleep.vmd");
    vmds["idle2"] = GetAnimation("resource/motion/mei_idle_think.vmd");
    vmds["idle3"] = GetAnimation("resource/motion/mei_idle_touch_clothes.vmd");
    vmds["idle4"] = GetAnimation("resource/motion/mei_idle_yawn.vmd");
    vmds["show"] = GetAnimation("resource/motion/mei_panel_on.vmd");
    SetAnimation("Agent","idle0",true,false);
}

void AppContext::Update(){
    autoPlayIdle();
	autoMonth();
	autoBlink();

	// Setup camera
	if (m_vmdCameraAnim)
	{
		m_vmdCameraAnim->Evaluate(m_animTime * FPS);
		const auto mmdCam = m_vmdCameraAnim->GetCamera();
		saba::MMDLookAtCamera lookAtCam(mmdCam);
		m_viewMat = glm::lookAt(lookAtCam.m_eye, lookAtCam.m_center, lookAtCam.m_up);
		m_projMat = glm::perspectiveFovRH(mmdCam.m_fov, float(m_screenWidth), float(m_screenHeight), 1.0f, 10000.0f);
	}
	else
	{
		m_viewMat = glm::lookAt(glm::vec3(0, 10, 50), glm::vec3(0, 10, 0), glm::vec3(0, 1, 0));
		m_projMat = glm::perspectiveFovRH(glm::radians(30.0f), float(m_screenWidth), float(m_screenHeight), 1.0f, 10000.0f);
	}

	for (auto& model : models)
	{
		// if(!model.second.has_setup)model.second.Setup(*this);
		// Update Animation
		model.second.UpdateAnimation(*this);

		// Update Vertices
		model.second.Update(*this);

		// Draw
		model.second.Draw(*this);
	}

    if(showPanel)DrawPanel();
}

void AppContext::BeforeUpdate(){
	double time = saba::GetTime();
	double elapsed = time - saveTime;
	if (elapsed > 1.0 / AppContext::FPS)
	{
		elapsed = 1.0 / AppContext::FPS;
	}
	saveTime = time;
	m_elapsed = float(elapsed);
	m_animTime += float(elapsed);

	glfwGetFramebufferSize(window, &m_screenWidth, &m_screenHeight);
	glViewport(0, 0, m_screenWidth, m_screenHeight);
}

void AppContext::BeforeEnd(){
	m_mmdShader.reset();
	m_mmdEdgeShader.reset();
	m_mmdGroundShadowShader.reset();

	for (auto& tex : m_textures)
	{
		glDeleteTextures(1, &tex.second.m_texture);
	}
	m_textures.clear();

	if (m_dummyColorTex != 0) { glDeleteTextures(1, &m_dummyColorTex); }
	if (m_dummyShadowDepthTex != 0) { glDeleteTextures(1, &m_dummyShadowDepthTex); }
	m_dummyColorTex = 0;
	m_dummyShadowDepthTex = 0;

	m_vmdCameraAnim.reset();
}

bool AppContext::AddModel(const string key,const string modelPath,const glm::vec3& t){
    Model model;
    model.setTranslate(t);
    auto ext = saba::PathUtil::GetExt(modelPath);
    if (ext == "pmd")
    {
            auto pmdModel = make_unique<saba::PMDModel>();
            if (!pmdModel->Load(modelPath, m_mmdDir))
            {
                    cout << "Failed to load pmd file.\n";
                    return false;
            }
            model.m_mmdModel = move(pmdModel);
    }
    else if (ext == "pmx")
    {
            auto pmxModel = make_unique<saba::PMXModel>();
            if (!pmxModel->Load(modelPath, m_mmdDir))
            {
                    cout << "Failed to load pmx file.\n";
                    return false;
            }
            model.m_mmdModel = move(pmxModel);
    }
    else
    {
            cout << "Unknown file type. [" << ext << "]\n";
            return false;
    }
    model.m_mmdModel->InitializeAnimation();
	model.m_vmdAnim = make_shared<saba::VMDAnimation>();
	model.m_vmdAnim->Create(model.m_mmdModel);
	model.m_vmdAnim->SyncPhysics(0.0f, FPS);
    //vmdAnim->SyncPhysics(0.0f);
    //model.m_vmdAnim = move(vmdAnim);
    model.Setup(*this);
	//[bug] if setup here will call the thread crash
    models[key] = move(model);
}

void AppContext::SetAnimation(const string key,saba::VMDFile & m_anim, bool loop,bool clear){
    if(clear){
        models[key].m_vmdAnim = make_shared<saba::VMDAnimation>();
        models[key].m_vmdAnim->Create(models[key].m_mmdModel);
    }
    models[key].m_vmdAnim->Add(m_anim);
    models[key].m_begin_anim_time = m_animTime;
	models[key].m_loop = loop;
}

void AppContext::SetAnimation(const string key,const string m_anim_key,bool loop,bool clear){
    SetAnimation(key,vmds[m_anim_key],loop,clear);
    cur_anim = m_anim_key;
}

void AppContext::autoPlayIdle(){
    if((sum_idle_time += m_elapsed) > 5.0f && cur_anim.find("idle") != -1){
        string anim_name = "idle" + to_string(rand() % 5);
        SetAnimation("Agent",anim_name,false,true);
        sum_idle_time -= 5.0f;
    }
}

Texture AppContext::GetTexture(const string & texturePath,bool flip){
	auto it = m_textures.find(texturePath);
	if (it == m_textures.end())
	{
		stbi_set_flip_vertically_on_load(flip);
		saba::File file;
		if (!file.Open(texturePath))
		{
			return Texture{ 0, false };
		}
		int x, y, comp;
		int ret = stbi_info_from_file(file.GetFilePointer(), &x, &y, &comp);
		if (ret == 0)
		{
			return Texture{ 0, false };
		}

		GLuint tex;
		glGenTextures(1, &tex);
		glBindTexture(GL_TEXTURE_2D, tex);

		int reqComp = 0;
		bool hasAlpha = false;
		if (comp != 4)
		{
			uint8_t* image = stbi_load_from_file(file.GetFilePointer(), &x, &y, &comp, STBI_rgb);
			glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, x, y, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
			stbi_image_free(image);
			hasAlpha = false;
		}
		else
		{
			uint8_t* image = stbi_load_from_file(file.GetFilePointer(), &x, &y, &comp, STBI_rgb_alpha);
			glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, x, y, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
			stbi_image_free(image);
			hasAlpha = true;
		}
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		glBindTexture(GL_TEXTURE_2D, 0);

		m_textures[texturePath] = Texture{ tex, hasAlpha };

		return m_textures[texturePath];
	}
	else
	{
		return (*it).second;
	}
}

saba::VMDFile AppContext::GetAnimation(const string vmdPath){
    saba::VMDFile vmdFile;
	if (!saba::ReadVMDFile(&vmdFile, vmdPath.c_str()))
	{
		cout << "Failed to read VMD file.\n";
	}
    if (!vmdFile.m_cameras.empty())
	{
		auto vmdCamAnim = make_unique<saba::VMDCameraAnimation>();
		if (!vmdCamAnim->Create(vmdFile))
		{
			cout << "Failed to create VMDCameraAnimation.\n";
		}
		m_vmdCameraAnim = move(vmdCamAnim);
	}
    return vmdFile;
}

bool AppContext::isAnimEnd(const string key){
	return (int)(m_animTime - models[key].m_begin_anim_time) * AppContext::FPS 
		>= models[key].m_vmdAnim->GetMaxKeyTime();
}

void AppContext::DeleteModel(const string key){
	models.erase(key);
}

void AppContext::DrawPanel(){
    ImGui::Begin("Morph Controller");
    auto model = models["Agent"].m_mmdModel;
    auto morphMan = model->GetMorphManager();
    size_t morphCount = morphMan->GetMorphCount();
    for (size_t morphIdx = 0; morphIdx < morphCount; morphIdx++)
    {
        auto morph = morphMan->GetMorph(morphIdx);
        float weight = morph->GetWeight();
        if (ImGui::SliderFloat((to_string(morphIdx)+morph->GetName().c_str()).c_str(), &weight, 0.0f, 1.0f))
        {
            morph->SetWeight(weight);
        }
    }
	ImGui::End();
}

void AppContext::PollMessage(Message message){
	if(message.fromType == AgentComponentType::AUDIO){
		string type = message.content.at(0);
		if(type == "StartPlay")SetAnimation("Agent","talk0",true,true),month_enable = true;
		if(type == "EndPlay")SetAnimation("Agent","idle0",true,true),month_enable = false;
	}else if(message.fromType == AgentComponentType::GUI){
		string type = message.content.at(0);
		if(type == "PANEL_ON")SetAnimation("Agent","show",false,true);
		if(type == "OPEN_MMD")showPanel = true;
	}
}

void AppContext::autoMonth(){
	if(month_enable){
		month->SetWeight(0.5f*(sin(m_animTime*month_speed)+1.0f));
	}else {
		month->SetWeight(0.0f);
	}
}

void AppContext::InitMorph(){
	auto model = models["Agent"].m_mmdModel;
    auto morphMan = model->GetMorphManager();
	month = morphMan->GetMorph(3);
	eye = morphMan->GetMorph(16);
}

void AppContext::autoBlink(){
	if(Blink_enable){
		eye->SetWeight(0.5f*(sin(m_animTime*Blink_speed)+1.0f));
		Blink_time += m_elapsed;
		if(Blink_time > Blink_last){
			Blink_time = 0.0f;
			Blink_enable = false;
			eye->SetWeight(0.0f);
		}
	}else{
		Blink_time += m_elapsed;
		if(Blink_time > Blink_interval){
			Blink_time = 0.0f;
			Blink_enable = true;
		}
	}
}